// Copyright 2021-2024 Buf Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import {
  codegenInfo,
  DescEnum,
  DescField,
  DescMessage,
  DescOneof,
} from "@bufbuild/protobuf";
import { LongType, ScalarType } from "@bufbuild/protobuf";
import {
  GeneratedFile,
  Printable, reifyWkt, RuntimeImports,
  Schema,
} from "@bufbuild/protoplugin/ecmascript";
import { localName } from "./runtime-import/names";
import { generateFieldInfo} from "./javascript.js";
import { getNonEditionRuntime } from "./editions.js";
import {
  getFieldTypeInfo,
  getFieldZeroValueExpression, getMessageType,
} from "./util.js";
import { createRuntimeImports } from "./runtime-import/rumtime-imports";


export const runtime = createRuntimeImports();

export function generateEts(schema: Schema) {
  (schema as any).prepareGenerate('ets');
  for (const runtimeKey in schema.runtime) {
    schema.runtime[runtimeKey as keyof RuntimeImports] = {
      ...schema.runtime[runtimeKey as keyof RuntimeImports],
      from: "@bitfun/protobuf_ets",
    }
  }
  (codegenInfo as any).packageName = "@bitfun/protobuf_ets";
  for (const file of schema.files) {
    const f = schema.generateFile(file.name + "_pb.ts");
    f.print(`import collections from "@arkts.collections"`)
    f.print();
    for (const enumeration of file.enums) {
      generateEnum(schema, f, enumeration);
    }
    for (const message of file.messages) {
      generateMessage(schema, f, message);
    }
  }
  const infoArr = (schema as any).getFileInfo();
  infoArr.forEach((info: { name: string; content: string }) => {
    info.name = info.name.slice(0, -3) + '.ets';
    info.content = info.content.split(/\r?\n/).map(line => {
      if (line.startsWith('import') && line.endsWith('_pb.js";')) {
        return line.replace('_pb.js', '_pb');
      }
      return line;
    }).join('\n');
  });
  (schema as any).getFileInfo = () => infoArr;
  (codegenInfo as any).packageName = "@bufbuild/protobuf";
}

// prettier-ignore
function generateEnum(schema: Schema, f: GeneratedFile, enumeration: DescEnum) {
  const {
    EnumValueInfo,
    EnumValueInfoInstance,
  } = runtime;
  
  f.print(f.exportDecl("const enum", enumeration), " {");
  for (const value of enumeration.values) {
    if (enumeration.values.indexOf(value) > 0) {
      f.print();
    }
    f.print(f.jsDoc(value, "  "));
    f.print("  ", localName(value), " = ", value.number, ",");
  }
  f.print("}");
  f.print();

  f.print("export const ", enumeration, "Array = [")
  for (const value of enumeration.values) {
    f.print('  "', localName(value), '"', enumeration.values.indexOf(value) != enumeration.values.length - 1 ? "," : "")
  }
  f.print("]");
  f.print()
  
  f.print("@Sendable");
  f.print(f.exportDecl("class", getMessageType(enumeration)), " {");
  f.print("  static enumType: collections.Array<", EnumValueInfo, "> = collections.Array.from([");
  for (const value of enumeration.values) {
    f.print("    new ", EnumValueInfoInstance, `(${value.number}, "${localName(value)}", "${localName(value)}"),`);
  }
  f.print("  ]);");
  f.print("}")
}

// prettier-ignore
function generateMessage(schema: Schema, f: GeneratedFile, message: DescMessage) {
  const protoN = getNonEditionRuntime(schema, message.file);
  const {
    FieldList,
    Message,
    MessageType,
    BinaryReadOptions,
    JsonReadOptions,
    FieldWrapper,
    ProtoRuntime,
    FieldInfo,
    getFieldListsFromArray,
    JsonValue,
  } = runtime;

  // 1. 先生成所有嵌套类型的完整定义
  for (const nestedEnum of message.nestedEnums) {
    generateEnum(schema, f, nestedEnum);
  }
  for (const nestedMessage of message.nestedMessages) {
    generateMessage(schema, f, nestedMessage);
  }

  // 2. 生成 MessageType 类
  f.print("@Sendable");
  f.print("export class ", getMessageType(message), " extends ", MessageType, "<", message, "> {");
  f.print("  static instance: ", getMessageType(message), " = new ", getMessageType(message), "();");
  f.print();
  f.print("  constructor() {");
  f.print("    super();");
  f.print("  }");
  f.print();
  f.print("  create(data?: ", Message, "<", message, ">): ", message, " {");
  f.print("    return new ",message,"(data);");
  f.print("  }");
  f.print();
  f.print("  equals(a: ", message, " | Message<", message, "> | undefined | null, b: ", message, " | Message<", message, "> | undefined | null): boolean {")
  f.print("    return ", protoN, ".util.equals(this, a, b);")
  f.print("  }");
  f.print();
  f.print("  clone(message: ", message, "): ", message," {")
  f.print("    return ", protoN, ".util.clone(message);")
  f.print("  }");
  f.print();


  f.print("  getFieldWrapper(): ", FieldWrapper, " | undefined {")
  f.print("    return ", message, ".fieldWrapper;")
  f.print("  }")
  f.print();
  f.print("  getTypeName(): string {")
  f.print("    return ", message, ".typeName;")
  f.print("  }")
  f.print();
  f.print("  getFields(): ", FieldList, " {")
  f.print("    return ", message, ".fields;")
  f.print("  }")
  f.print();
  f.print("  getProtoRuntime(): ", ProtoRuntime, " {")
  f.print("    return ", message, ".runtime;")
  f.print("  }")
  f.print("}");
  f.print();

  // 3. 生成实际的消息类
  f.print("@Sendable");
  f.print("export class ", message, " extends ", Message, "<", message, "> {");
  
  // 生成字段
  for (const member of message.members) {
    switch (member.kind) {
      case "oneof":
        generateOneof(schema, f, member);
        break;
      default:
        generateField(schema, f, member);
        break;
    }
    f.print();
  }

  // 生成静态字段和方法
  f.print("  static fieldWrapper: ", FieldWrapper, "<", message, ", string> | undefined;");
  f.print();
  f.print("  static readonly typeName: string = ", f.string(message.typeName), ";");
  f.print();
  f.print("  static readonly runtime: ", ProtoRuntime, " = ", protoN, ";");
  f.print();
  f.print("  static readonly fieldList: collections.Map<string, ", FieldInfo, ">", " = new collections.Map<string, ", FieldInfo, ">();");
  f.print();
  f.print("  static fields: FieldList;");
  f.print();
  f.print("  static isInitialized: boolean = false;");
  f.print();
  f.print("  static getFields(): ", FieldList, " {");
  f.print("    return ", protoN, ".util.newFieldList(", getFieldListsFromArray, "(() => { ");
  f.print("      let res = new collections.Array<", FieldInfo, ">();");
  for (const field of message.fields){
    generateFieldInfo(schema, f, field as DescField);
  }
  f.print("      return res;");
  f.print("    }));")
  f.print("  }")
  f.print();
  // 生成构造函数和实例方法
  f.print("  constructor(data?: ", Message, "<", message, ">) {");
  f.print("    super();");
  f.print("    ", protoN, ".util.initPartial(data, this);");
  f.print("    if(!", message, ".isInitialized) {")
  f.print("      ", message, ".fields = ", message, ".getFields();")
  f.print("      ", message, ".isInitialized = true;")
  f.print("    }");
  f.print("  }");
  f.print();
  f.print("  getType(): ", MessageType, "<", message,"> {");
  f.print("    return ", getMessageType(message), ".instance;");
  f.print("  }");
  
  // 生成特殊方法
  generateWktMethods(schema, f, message);
  f.print();
  generateWktStaticMethods(schema, f, message);
  
  // 生成静态工具方法
  f.print("  static fromBinary(bytes: Uint8Array, options?: Partial<", BinaryReadOptions, ">): ", message, " {")
  f.print("    return new ", message, "().getType().fromBinary(bytes, options);")
  f.print("  }")
  f.print()
  f.print("  static fromJson(jsonValue: ", JsonValue, ", options?: Partial<", JsonReadOptions, ">): ", message, " {")
  f.print("    return new ", message, "().getType().fromJson(jsonValue, options);")
  f.print("  }")
  f.print()
  f.print("  static fromJsonString(jsonString: string, options?: Partial<", JsonReadOptions, ">): ", message, " {")
  f.print("    return new ", message, "().getType().fromJsonString(jsonString, options);")
  f.print("  }")
  f.print()
  f.print("  static equals(a: ", message , " | undefined, b: ", message , " | undefined): boolean {")
  f.print("    return ", protoN, ".util.equals(", getMessageType(message), ".instance, a, b);")
  f.print("  }")
  f.print("}")
  f.print()
}

// prettier-ignore
function generateOneof(schema: Schema, f: GeneratedFile, oneof: DescOneof) {
  f.print(f.jsDoc(oneof, "  "));
  const {
    OneofClass
  } = runtime;
  f.print("  ", localName(oneof), ": ", OneofClass, "<");
  for (const field of oneof.fields) {
    const isLast = oneof.fields.indexOf(field) === oneof.fields.length - 1;
    const { typing } = getFieldTypeInfo(field);
    f.print("    ", typing, isLast ? ['> = new ', OneofClass, "();"] : ' |');
  }
}

// prettier-ignore
function generateField(schema: Schema, f: GeneratedFile, field: DescField) {
  f.print(f.jsDoc(field, "  "));
  const e: Printable = [];
  const { typing, optional } = getFieldTypeInfo(field);
  if (optional) {
    e.push("  ", localName(field), "?: ", typing, ";");
  } else {
    e.push("  ", localName(field), ": ", typing);
    const zeroValue = getFieldZeroValueExpression(field);
    if (zeroValue !== undefined) {
      e.push(" = ", zeroValue);
    }
    e.push(";");
  }
  f.print(e);
}

// prettier-ignore
function generateWktMethods(schema: Schema, f: GeneratedFile, message: DescMessage) {
  const ref = reifyWkt(message);
  if (ref === undefined) {
    return;
  }
  const {
    JsonValue,
    JsonReadOptions,
    JsonWriteOptions,
    JsonObject,
    ScalarType: rtScalarType,
    LongType: rtLongType,
    protoInt64,
  } = schema.runtime;
  const protoN = getNonEditionRuntime(schema, message.file);
  switch (ref.typeName) {
    case "google.protobuf.Timestamp":
      f.print("  override fromJson(json: ", JsonValue, ", options?: Partial<", JsonReadOptions, ">): this {");
      f.print(`    if (typeof json !== "string") {`);
      f.print("      throw new Error(`cannot decode ", message.typeName, " from JSON: ${", protoN, ".json.debug(json)}`);");
      f.print("    }");
      f.print(`    const matches = json.match(/^([0-9]{4})-([0-9]{2})-([0-9]{2})T([0-9]{2}):([0-9]{2}):([0-9]{2})(?:Z|\\.([0-9]{3,9})Z|([+-][0-9][0-9]:[0-9][0-9]))$/);`);
      f.print("    if (!matches) {");
      f.print("      throw new Error(`cannot decode ", message.typeName, " from JSON: invalid RFC 3339 string`);");
      f.print("    }");
      f.print(`    const ms = Date.parse(matches[1] + "-" + matches[2] + "-" + matches[3] + "T" + matches[4] + ":" + matches[5] + ":" + matches[6] + (matches[8] ? matches[8] : "Z"));`);
      f.print("    if (Number.isNaN(ms)) {");
      f.print("      throw new Error(`cannot decode ", message.typeName, " from JSON: invalid RFC 3339 string`);");
      f.print("    }");
      f.print(`    if (ms < Date.parse("0001-01-01T00:00:00Z") || ms > Date.parse("9999-12-31T23:59:59Z")) {`);
      f.print("      throw new Error(`cannot decode message ", message.typeName, " from JSON: must be from 0001-01-01T00:00:00Z to 9999-12-31T23:59:59Z inclusive`);");
      f.print("    }");
      if (ref.seconds.longType === LongType.STRING) {
        f.print("    this.", localName(ref.seconds), " = ", protoInt64, ".parse(ms / 1000).toString();");
      } else {
        f.print("    this.", localName(ref.seconds), " = ", protoInt64, ".parse(ms / 1000);");
      }
      f.print("    this.", localName(ref.nanos), " = 0;");
      f.print("    if (matches[7]) {");
      f.print(`      this.`, localName(ref.nanos), ` = (parseInt("1" + matches[7] + "0".repeat(9 - matches[7].length)) - 1000000000);` );
      f.print("    }");
      f.print("    return this;");
      f.print("  }");
      f.print();
      f.print("  override toJson(options?: Partial<", JsonWriteOptions, ">): JsonValue {");
      f.print("    const ms = Number(this.", localName(ref.seconds), ") * 1000;");
      f.print(`    if (ms < Date.parse("0001-01-01T00:00:00Z") || ms > Date.parse("9999-12-31T23:59:59Z")) {`);
      f.print("      throw new Error(`cannot encode ", message.typeName, " to JSON: must be from 0001-01-01T00:00:00Z to 9999-12-31T23:59:59Z inclusive`);");
      f.print("    }");
      f.print("    if (this.", localName(ref.nanos), " < 0) {");
      f.print("      throw new Error(`cannot encode ", message.typeName, " to JSON: nanos must not be negative`);");
      f.print("    }");
      f.print(`    let z = "Z";`);
      f.print("    if (this.", localName(ref.nanos), " > 0) {");
      f.print("      const nanosStr = (this.", localName(ref.nanos), " + 1000000000).toString().substring(1);");
      f.print(`      if (nanosStr.substring(3) === "000000") {`);
      f.print(`        z = "." + nanosStr.substring(0, 3) + "Z";`);
      f.print(`      } else if (nanosStr.substring(6) === "000") {`);
      f.print(`        z = "." + nanosStr.substring(0, 6) + "Z";`);
      f.print("      } else {");
      f.print(`        z = "." + nanosStr + "Z";`);
      f.print("      }");
      f.print("    }");
      f.print(`    return new Date(ms).toISOString().replace(".000Z", z);`);
      f.print("  }");
      f.print();
      f.print("  toDate(): Date {");
      f.print("    return new Date(Number(this.", localName(ref.seconds), ") * 1000 + Math.ceil(this.", localName(ref.nanos), " / 1000000));");
      f.print("  }");
      f.print();
      break;
    case "google.protobuf.Duration":
      f.print("  override fromJson(json: ", JsonValue, ", options?: Partial<", JsonReadOptions, ">): this {")
      f.print(`    if (typeof json !== "string") {`)
      f.print("      throw new Error(`cannot decode ", message.typeName, " from JSON: ${proto3.json.debug(json)}`);")
      f.print("    }")
      f.print(`    const match = json.match(/^(-?[0-9]+)(?:\\.([0-9]+))?s/);`)
      f.print("    if (match === null) {")
      f.print("      throw new Error(`cannot decode ", message.typeName, " from JSON: ${", protoN, ".json.debug(json)}`);")
      f.print("    }")
      f.print("    const longSeconds = Number(match[1]);")
      f.print("    if (longSeconds > 315576000000 || longSeconds < -315576000000) {")
      f.print("      throw new Error(`cannot decode ", message.typeName, " from JSON: ${", protoN, ".json.debug(json)}`);")
      f.print("    }")
      if (ref.seconds.longType === LongType.STRING) {
        f.print("    this.", localName(ref.seconds), " = ", protoInt64, ".parse(longSeconds).toString();")
      } else {
        f.print("    this.", localName(ref.seconds), " = ", protoInt64, ".parse(longSeconds);")
      }
      f.print(`    if (typeof match[2] == "string") {`)
      f.print(`      const nanosStr = match[2] + "0".repeat(9 - match[2].length);`)
      f.print("      this.", localName(ref.nanos), " = parseInt(nanosStr);")
      f.print("      if (longSeconds < 0 || Object.is(longSeconds, -0)) {");
      f.print("        this.", localName(ref.nanos), " = -this.", localName(ref.nanos), ";")
      f.print("      }")
      f.print("    }")
      f.print("    return this;")
      f.print("  }")
      f.print()
      f.print("  override toJson(options?: Partial<", JsonWriteOptions, ">): JsonValue {")
      f.print("    if (Number(this.", localName(ref.seconds), ") > 315576000000 || Number(this.", localName(ref.seconds), ") < -315576000000) {")
      f.print("      throw new Error(`cannot encode ", message.typeName, " to JSON: value out of range`);")
      f.print("    }")
      f.print("    let text = this.", localName(ref.seconds), ".toString();")
      f.print("    if (this.", localName(ref.nanos), " !== 0) {")
      f.print("      let nanosStr = Math.abs(this.", localName(ref.nanos), ").toString();")
      f.print(`      nanosStr = "0".repeat(9 - nanosStr.length) + nanosStr;`)
      f.print(`      if (nanosStr.substring(3) === "000000") {`)
      f.print("        nanosStr = nanosStr.substring(0, 3);")
      f.print(`      } else if (nanosStr.substring(6) === "000") {`)
      f.print("        nanosStr = nanosStr.substring(0, 6);")
      f.print(`      }`)
      f.print(`      text += "." + nanosStr;`)
      f.print("      if (this.", localName(ref.nanos), " < 0 && Number(this.", localName(ref.seconds), ") == 0) {");
      f.print(`          text = "-" + text;`);
      f.print(`      }`);
      f.print("    }")
      f.print(`    return text + "s";`)
      f.print("  }")
      f.print()
      break;
    case "google.protobuf.Struct":
      f.print("  override toJson(options?: Partial<", JsonWriteOptions, ">): ", JsonValue, " {")
      f.print("    const json: ", JsonObject, " = {}")
      f.print("    for (const [k, v] of Object.entries(this.", localName(ref.fields), ")) {")
      f.print("      json[k] = v.toJson(options);")
      f.print("    }")
      f.print("    return json;")
      f.print("  }")
      f.print()
      f.print("  override fromJson(json: ", JsonValue, ", options?: Partial<", JsonReadOptions, ">): this {")
      f.print(`    if (typeof json != "object" || json == null || Array.isArray(json)) {`)
      f.print(`      throw new Error("cannot decode `, message.typeName, ` from JSON " + `, protoN, `.json.debug(json));`)
      f.print("    }")
      f.print("    for (const [k, v] of Object.entries(json)) {")
      f.print("      this.", localName(ref.fields), "[k] = ", ref.fields.mapValue.message ?? "", ".fromJson(v);")
      f.print("    }")
      f.print("    return this;")
      f.print("  }")
      f.print()
      break;
    case "google.protobuf.DoubleValue":
    case "google.protobuf.FloatValue":
    case "google.protobuf.Int64Value":
    case "google.protobuf.UInt64Value":
    case "google.protobuf.Int32Value":
    case "google.protobuf.UInt32Value":
    case "google.protobuf.BoolValue":
    case "google.protobuf.StringValue":
    case "google.protobuf.BytesValue":
      f.print("  override toJson(options?: Partial<", JsonWriteOptions, ">): ", JsonValue, " {")
      f.print("    return proto3.json.writeScalar(", rtScalarType, ".", ScalarType[ref.value.scalar], ", this.value, true)!;")
      f.print("  }")
      f.print()
      f.print("  override fromJson(json: ", JsonValue, ", options?: Partial<", JsonReadOptions, ">): this {")
      f.print("    try {")
      if (ref.value.longType === LongType.STRING) {
        f.print("      this.value = ", protoN, ".json.readScalar(", rtScalarType, ".", ScalarType[ref.value.scalar], ", json, ", rtLongType, ".", LongType[ref.value.longType] ,");")
      } else {
        f.print("      this.value = ", protoN, ".json.readScalar(", rtScalarType, ".", ScalarType[ref.value.scalar], ", json);")
      }
      f.print("    } catch (e) {")
      f.print("      let m = `cannot decode message ", message.typeName, " from JSON\"`;")
      f.print("      if (e instanceof Error && e.message.length > 0) {")
      f.print("        m += `: ${e.message}`")
      f.print("      }")
      f.print("      throw new Error(m);")
      f.print("    }")
      f.print("    return this;")
      f.print("  }")
      f.print()
      break;
  }
}

// prettier-ignore
function generateWktStaticMethods(schema: Schema, f: GeneratedFile, message: DescMessage) {
  const ref = reifyWkt(message);
  if (ref === undefined) {
    return;
  }
  const {
    protoInt64,
  } = schema.runtime;
  switch (ref.typeName) {
    case "google.protobuf.Timestamp":
      f.print("  static now(): ", message, " {")
      f.print("    return ", message, ".fromDate(new Date())")
      f.print("  }")
      f.print()
      f.print("  static fromDate(date: Date): ", message, " {")
      f.print("    const ms = date.getTime();")
      f.print("    return new ", message, "({")
      if (ref.seconds.longType === LongType.STRING) {
        f.print("      ", localName(ref.seconds), ": ", protoInt64, ".parse(Math.floor(ms / 1000)).toString(),")
      } else {
        f.print("      ", localName(ref.seconds), ": ", protoInt64, ".parse(Math.floor(ms / 1000)),")
      }
      f.print("      ", localName(ref.nanos), ": (ms % 1000) * 1000000,")
      f.print("    });")
      f.print("  }")
      f.print()
      break;
    case "google.protobuf.DoubleValue":
    case "google.protobuf.FloatValue":
    case "google.protobuf.Int64Value":
    case "google.protobuf.UInt64Value":
    case "google.protobuf.Int32Value":
    case "google.protobuf.UInt32Value":
    case "google.protobuf.BoolValue":
    case "google.protobuf.StringValue":
    case "google.protobuf.BytesValue": {
      const {typing} = getFieldTypeInfo(ref.value);
      f.print("  static readonly fieldWrapper = {")
      f.print("    wrapField(value: ", typing, "): ", message, " {")
      f.print("      return new ", message, "({value});")
      f.print("    },")
      f.print("    unwrapField(value: ", message, "): ", typing, " {")
      f.print("      return value.", localName(ref.value), ";")
      f.print("    }")
      f.print("  };")
      f.print()
      break;
    }
    case "google.protobuf.Duration":
    case "google.protobuf.Struct":
  }
}
